/* Routines for client control of ohaigent.
 *
 * Copyright 2008 Cian Synnott.
 * 
 * $Id: client.c 28 2009-12-04 03:01:17Z cian $
 */
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/queue.h>
#include <sys/un.h>
#include <unistd.h>

#include <event.h>

#include "client.h"
#include "log.h"
#include "utility.h"
#include "write_self.h"
#include "xmpp.h"

#define CLIENT_BUFSIZE 1024

struct _client_session {
  int fd;
  char *path;
  struct event *evt;
};

struct client_command {
  char *command;
  void (*function)(int fd, char *arguments, xmpp_session *sess);
};

static void client_command_send(int fd, char *arguments, xmpp_session *sess) {
  char *send_to = arguments;
  char *send_text = remainder_after_first_token(arguments);

  if (strlen(send_to) == 0) {
    log_err("received SEND with no JID from fd %d", fd);
    return;
  }

  if (strlen(send_text) == 0) {
    log_err("received SEND to JID '%s' with no text from fd %d", send_to, fd);
    return;
  }
  xmpp_send_message(sess, send_to, send_text);
}

static void client_command_messages(int fd, char *arguments, xmpp_session *sess) {
  xmpp_user_message *current, *tmp;
  char buffer[CLIENT_BUFSIZE];
  int written;

  current = xmpp_get_user_messages(sess);

  socket_write(fd, "BEGIN\n");

  while (current) {
    written = snprintf(buffer, CLIENT_BUFSIZE, "ITEM %s\n", current->message);
    if (written < 0 || written >= CLIENT_BUFSIZE) {
      log_err("Error constructing an ITEM string for MESSAGES response.");
      continue;
    }

    socket_write(fd, buffer);
   
    /* Free the list as we go. */
    tmp = current->next;
    free(current->message);
    free(current);
    current = tmp;
  }

  socket_write(fd, "END\n");
}

static void client_command_who(int fd, char *arguments, xmpp_session *sess) {
  xmpp_status *current;
  char buffer[CLIENT_BUFSIZE];
  int written;

  current = xmpp_get_contact_status(sess);

  socket_write(fd, "BEGIN\n");

  while (current) {
    if (current->message)
      written = snprintf(buffer, CLIENT_BUFSIZE, "ITEM %s %s %s\n",
        current->jid, current->show, current->message);
    else
      written = snprintf(buffer, CLIENT_BUFSIZE, "ITEM %s %s\n",
        current->jid, current->show);
    if (written < 0 || written >= CLIENT_BUFSIZE) {
      log_err("Error constructing an ITEM string for WHO response.");
      continue;
    }
    socket_write(fd, buffer);

    current = current->next;
  }

  socket_write(fd, "END\n");
}

static void client_dispatch_command(int fd, char *command, xmpp_session *sess) {
  static struct client_command command_map[] = {
    { "SEND", client_command_send },
    { "MESSAGES", client_command_messages },
    { "WHO", client_command_who },
    {  NULL,  NULL}
  };
  char *remainder;
  int i, valid;

  log_debug("received command from fd %d: '%s'", fd, command);

  remainder = remainder_after_first_token(command);

  if (strlen(command) == 0) {
    log_err("received empty command from fd %d");
    return;
  }

  valid = 0;
  for (i = 0; command_map[i].command != NULL; i++)
    if (!strcmp(command, command_map[i].command)) {
      command_map[i].function(fd, remainder, sess);
      valid = 1;
      break;
    }

  if (!valid)
    log_err("received invalid command from fd %d");
}

/* Basic PoC for 'ohai' write replacement client. */
static void client_process_data(int fd, short event, void *arg) {
  char buffer[CLIENT_BUFSIZE];
  int len;

  if ((len = socket_read(fd, buffer, CLIENT_BUFSIZE)) == -1) {
    log_err("socket_read failed for fd %d", fd);
    return;
  }

  if (0 == len) {
    if (close(fd) == -1)
      log_err("error closing client fd %d: %s", fd, strerror(errno));
    return;
  }

  kill_newline(buffer);

  client_dispatch_command(fd, buffer, arg);

  if (event_once(fd, EV_READ, client_process_data, arg, NULL) == -1)
    log_errx(EXIT_FAILURE, "registering client_process_data for fd %d with event loop", fd);
}

static void client_accept_connection(int fd, short event, void *arg) {
  int sock = accept(fd, NULL, NULL);
  if (-1 == sock) {
    log_warn("accepting on fd %d: %s", fd, strerror(errno));
    return;
  }
  if (event_once(sock, EV_READ, client_process_data, arg, NULL) == -1)
    log_errx(EXIT_FAILURE, "registering client_process_data for fd %d with event loop", sock);
}

/* See client.h */
#define LISTEN_QUEUE_LENGTH 10
client_session *client_setup(xmpp_session *xmpp, char *path) {
  struct sockaddr_un addr_un;
  client_session *sess;

  sess = calloc(1, sizeof(client_session));
  if (sess == NULL)
    log_errx(EXIT_FAILURE, "calloc() failed allocating a new client_session: %s", strerror(errno));

  sess->path = strdup(path);
  sess->evt = calloc(1, sizeof(struct event));
  if (sess->evt == NULL)
    log_errx(EXIT_FAILURE, "calloc() failed allocating a new struct event: %s", strerror(errno));

  log_info("binding UNIX domain server at '%s'", path);
  if ((sess->fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
    log_errx(EXIT_FAILURE, "creating UNIX socket %s: %s", path, strerror(errno));

  if (set_nonblocking(sess->fd) == -1)
    log_errx(EXIT_FAILURE, "setting UNIX socket %s nonblocking: %s", path, strerror(errno));

  addr_un.sun_family = AF_UNIX;
  strncpy(addr_un.sun_path, path, sizeof(addr_un.sun_path));
  if (bind(sess->fd, (struct sockaddr *) &addr_un, sizeof(struct sockaddr_un)) == -1)
    log_errx(EXIT_FAILURE, "binding UNIX socket %s nonblocking: %s", path, strerror(errno));

  if (listen(sess->fd, LISTEN_QUEUE_LENGTH) == -1)
    log_errx(EXIT_FAILURE, "listening on UNIX socket %s: %s", path, strerror(errno));

  log_info("registering UNIX domain server at '%s' with event loop as fd %d",
    path, sess->fd);

  event_set(sess->evt, sess->fd, EV_READ | EV_PERSIST, client_accept_connection, xmpp);
  if (event_add(sess->evt, NULL) == -1)
    log_errx(EXIT_FAILURE, "registering client_accept_connection for fd %d with event loop", sess->fd);

  return sess;
}

/* See client.h */
void client_teardown(client_session *sess) {
  if (event_del(sess->evt) == -1)
    log_err("event_del failed for client session event");
  free(sess->evt);
  if (close(sess->fd) == -1)
    log_err("close() failed for client session listen socket fd %d", sess->fd);
  if (unlink(sess->path) == -1)
    log_err("unlink() failed for client session listen socket '%s'", sess->path);
  free(sess->path);
  free(sess);
}
